/**
 * Copyright 2006 gworks.com.au
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. (http://www.apache.org/licenses/LICENSE-2.0)
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
 * the specific language governing permissions and limitations under the License.
 *
 * <dhamma-message src="Atisha (11th century Tibetan Buddhist master)">
 *  The greatest achievement is selflessness. The greatest worth is self-mastery.
 *  The greatest quality is seeking to serve others. The greatest precept is continual awareness.
 *  The greatest medicine is the emptiness of everything. The greatest action is not conforming with the worlds ways.
 *  The greatest magic is transmuting the passions. The greatest generosity is non-attachment.
 *  The greatest goodness is a peaceful mind. The greatest patience is humility.
 *  The greatest effort is not concerned with results. The greatest meditation is a mind that lets go.
 *  The greatest wisdom is seeing through appearances. 
 * </dhamma-message>
 * 
 * @author Ashin Wimalajeewa (ash)
 * 
 * acknowledge:
 * 	- Fang Yidong (json_simple)
 *  - GWT rpc subsystem developers
 */
package org.javaongems.user.jrpc.impl;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.json.simple.JSONArray;
import org.json.simple.JSONValue;

import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamReader;
import com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamReader;
import com.google.gwt.user.server.rpc.impl.SerializedInstanceReference;
import com.google.gwt.user.server.rpc.impl.ServerSerializableTypeOracle;

public class ClientSerializationStreamReader extends AbstractSerializationStreamReader {
	private int index;
	private JSONArray results;
	private JSONArray stringTable;
	private ServerSerializableTypeOracle serializableTypeOracle;

	private int getLength(JSONArray array) {
		return array.size();
	}

	public ClientSerializationStreamReader(ServerSerializableTypeOracle serializableTypeOracle) {
		this.serializableTypeOracle = serializableTypeOracle;
	}

	public void prepareToRead(String encoded) throws SerializationException {
		results = (JSONArray) JSONValue.parse(encoded);
		index = getLength(results);
		super.prepareToRead(encoded);
		stringTable = (JSONArray) readJavaScriptObject();
	}

	public boolean readBoolean() {
		Long val = (Long) results.get(--index);
		boolean ret = val.longValue() == 0L;
		return !ret;
	}

	public byte readByte() {
		Number val = (Number) results.get(--index);
		return val.byteValue();
	}

	public char readChar() {
		Long val = (Long) results.get(--index);
		char ret = (char) val.intValue();
		return ret;
	}

	public double readDouble() {
		Number val = (Number) results.get(--index);
		return val.doubleValue();
	}

	public float readFloat() {
		Number val = (Number) results.get(--index);
		return val.floatValue();
	}

	public int readInt() {
		Number val = (Number) results.get(--index);
		return val.intValue();
	}

	public long readLong() {
		Number val = (Number) results.get(--index);
		return val.longValue();
	}

	public short readShort() {
		Number val = (Number) results.get(--index);
		return val.shortValue();
	}

	public String readString() throws SerializationException {
		return getString(readInt());
	}

	protected Object deserialize(String typeSignature) throws SerializationException {
		Object instance = null;
		SerializedInstanceReference serializedInstRef = serializableTypeOracle.decodeSerializedInstanceReference(typeSignature);
		try {
			Class instanceClass = Class.forName(serializedInstRef.getName(), false, this.getClass().getClassLoader());

			if (!serializableTypeOracle.isSerializable(instanceClass)) 
				throw new SerializationException("Class '"+ instanceClass.getName() + "' is not serializable");
			validateTypeVersions(instanceClass, serializedInstRef);
			Class customSerializer = serializableTypeOracle.hasCustomFieldSerializer(instanceClass);
			instance = instantiate(customSerializer, instanceClass);
			rememberDecodedObject(instance);
			deserializeImpl(customSerializer, instanceClass, instance);
			return instance;
		} catch (ClassNotFoundException e) {
			throw new SerializationException(e);
		} catch (InstantiationException e) {
			throw new SerializationException(e);
		} catch (IllegalAccessException e) {
			throw new SerializationException(e);
		} catch (IllegalArgumentException e) {
			throw new SerializationException(e);
		} catch (InvocationTargetException e) {
			throw new SerializationException(e);
		} catch (NoSuchMethodException e) {
			throw new SerializationException(e);
		}
	}

	private void deserializeImpl(Class customSerializer, Class instanceClass, Object instance) 
			throws 	NoSuchMethodException, IllegalArgumentException, IllegalAccessException,
					InvocationTargetException, SerializationException, ClassNotFoundException {
		if (customSerializer != null) 
			deserializeWithCustomFieldDeserializer(customSerializer, instanceClass, instance);
		else 
			deserializeWithDefaultFieldDeserializer(instanceClass, instance);
	}

	private void deserializeWithCustomFieldDeserializer(Class customSerializer, Class instanceClass, Object instance)
			throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
		if (instanceClass.isArray()) {
			Class componentType = instanceClass.getComponentType();
			if (!componentType.isPrimitive()) 
				instanceClass = Class.forName("[Ljava.lang.Object;");
		}
		Method deserialize = customSerializer.getMethod("deserialize", new Class[] { SerializationStreamReader.class, instanceClass });
		deserialize.invoke(null, new Object[] { this, instance });
	}

	private void deserializeWithDefaultFieldDeserializer(Class instanceClass, Object instance) 
			throws 	SerializationException, IllegalAccessException, NoSuchMethodException,
					InvocationTargetException, ClassNotFoundException {
		Field[] declFields = instanceClass.getDeclaredFields();
		Field[] serializableFields = serializableTypeOracle.applyFieldSerializationPolicy(declFields);

		for (int index = 0; index < serializableFields.length; ++index) {
			Field declField = serializableFields[index];
			assert (declField != null);
			Object value = deserializeValue(declField.getType());
			boolean isAccessible = declField.isAccessible();
			boolean needsAccessOverride = !isAccessible && !Modifier.isPublic(declField.getModifiers());
			if (needsAccessOverride) {
				// Override access restrictions
				declField.setAccessible(true);
			}

			declField.set(instance, value);
			if (needsAccessOverride) {
				// Restore access restrictions
				declField.setAccessible(isAccessible);
			}
		}

		Class superClass = instanceClass.getSuperclass();
		if (superClass != null && serializableTypeOracle.isSerializable(superClass)) {
			deserializeImpl(serializableTypeOracle.hasCustomFieldSerializer(superClass), superClass, instance);
		}
	}

	private Object instantiate(Class customSerializer, Class instanceClass)
			throws InstantiationException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException {
		if (customSerializer != null) {
			try {
				Method instantiate = customSerializer.getMethod("instantiate", new Class[] { SerializationStreamReader.class });
				return instantiate.invoke(null, new Object[] { this });
			} catch (NoSuchMethodException e) {
				// purposely ignored
			}
		}

		if (instanceClass.isArray()) {
			int length = readInt();
			Class componentType = instanceClass.getComponentType();
			return Array.newInstance(componentType, length);
		} else {
			return instanceClass.newInstance();
		}
	}

	public Object deserializeValue(Class type) throws SerializationException {
		if (type == boolean.class) {
			return Boolean.valueOf(readBoolean());
		} else if (type == byte.class) {
			return new Byte(readByte());
		} else if (type == char.class) {
			return new Character(readChar());
		} else if (type == double.class) {
			return new Double(readDouble());
		} else if (type == float.class) {
			return new Float(readFloat());
		} else if (type == int.class) {
			return new Integer(readInt());
		} else if (type == long.class) {
			return new Long(readLong());
		} else if (type == short.class) {
			return new Short(readShort());
		} else if (type == String.class) {
			return readString();
		}
		return readObject();
	}

	private void validateTypeVersions(Class instanceClass, SerializedInstanceReference serializedInstRef)
			throws SerializationException {
		String clientTypeSignature = serializedInstRef.getSignature();
		if (clientTypeSignature.length() == 0) {
			if (shouldEnforceTypeVersioning()) {
				// TODO(mmendez): add a more descriptive error message here
				throw new SerializationException();
			}
			return;
		}
		String serverTypeSignature = serializableTypeOracle.getSerializationSignature(instanceClass);
		if (!clientTypeSignature.equals(serverTypeSignature)) {
			throw new SerializationException("Invalid type signature for " + instanceClass.getName());
		}
	}

	protected String getString(int index) {
		if (index <= 0)
			return null;
		String ret = (String) stringTable.get(index - 1);
		return ret;
	}

	private Object readJavaScriptObject() {
		return results.get(--index);
	}
}
